﻿using System;
using System.Text;
using System.Collections.Generic;
using System.IO;

namespace reb0x
{
    public class RebirthFileReader
    {
        // Number of 303s in a rebirth song file
        public const uint NUMBER_OF_303S = 2;

        // Number of patterns in each rebirth 303
        public const uint PATTERNS_PER_303 = 32;

        private BinaryReader _reader;
        private readonly List<Pattern> _patterns = new List<Pattern>(Convert.ToInt16(NUMBER_OF_303S * PATTERNS_PER_303));

        public RebirthFileReader(string filePath)
        {
            using (_reader = new BinaryReader(File.Open(filePath, FileMode.Open), Encoding.ASCII))
            {
                // first should be "CAT "
                string tag = ReadChunkMarker();
                VerifyTagMatches(tag, "CAT ");

                // then comes the length of the RB40 chunk
                /*uint rb40Length =*/
                ReadChunkLength();

                // followed by the RB40 chunk marker
                tag = ReadChunkMarker();
                VerifyTagMatches(tag, "RB40");

                // order of items in RB40 catalog level is not fixed, 
                // although ReBirth 2.0.1 writes in a specific order... HEAD, GLOB, USRI, CAT DEVL, CAT TRKL

                tag = ReadChunkMarker();
                while (tag != "CAT ")
                {
                    Console.WriteLine("Skipping '" + tag + "' section...");
                    // read the size of this cat chunk
                    uint length = ReadChunkLength();
                    ReadChunk(length);
                    tag = ReadChunkMarker();
                }

                // read the size of this cat chunk
                uint catLength = ReadChunkLength();
                // read the type, this should be the device list
                tag = ReadChunkMarker();
                VerifyTagMatches(tag, "DEVL");
                VerifyChunkLength(catLength, 14850);
                Console.WriteLine("Found 'DEVL' (device list) section.");
                // next is a series of tags which we ignore.
                // their order is fixed.
                string[] tags = new string[] { "MIXR", "DELY", "PCF ", "DIST", "COMP" };
                foreach (string expectedTag in tags)
                {
                    tag = ReadChunkMarker();
                    VerifyTagMatches(tag, expectedTag);
                    ReadChunk(ReadChunkLength());
                }

                // at this point, we should be ready to get what we're after, 
                // the 303 chunks!

                // for each 303...
                for (uint i = 0; i < NUMBER_OF_303S; ++i)
                {
                    Console.WriteLine("Reading 303 data, " + (i + 1) + " of " + NUMBER_OF_303S);
                    tag = ReadChunkMarker();
                    VerifyTagMatches(tag, "303 ");
                    uint t0tChunkLength = ReadChunkLength();
                    VerifyChunkLength(t0tChunkLength, 1097);
                    // skip 9 bytes of info we can't use
                    ReadChunk(9);

                    // from here on we read the patterns' data
                    for (int pat = 0; pat < PATTERNS_PER_303; ++pat)
                    {
                        Console.WriteLine("Reading pattern " + (pat + 1) + " of " + PATTERNS_PER_303);
                        Pattern pattern = new Pattern();

                        // first byte is shuffle on/off data, which we ignore [yet].
                        _reader.ReadByte();
                        // second byte is pattern length
                        pattern.Length = _reader.ReadByte();

                        // then we have 16 steps
                        for (byte step = 0; step < Pattern.STEPS_PER_PATTERN; ++step)
                        {
                            Console.Write("Step " + (step + 1) + " of " + Pattern.STEPS_PER_PATTERN);

                            // each step is 2 bytes
                            // the first byte is the note/tone information (from C1 to C2)
                            // the second one is a bitmask for slide/accent/rest/oct up/oct down.. etc
                            byte note = _reader.ReadByte();
                            Console.Write("  Note = " + note);
                            byte flags = _reader.ReadByte();
                            Console.WriteLine(". Flags = " + flags);

                            // extract the flags...
                            // bit 0 = No slide/Slide (0x01)
                            bool slide = (flags & 0x01) == 0x01;
                            // bit 1 = No accent/Accent (0x02)
                            bool accent = (flags & 0x02) == 0x02;
                            // bit 2 = Normal/Transpose up (0x04)
                            bool transposeUp = (flags & 0x04) == 0x04;
                            // bit 3 = Normal/Transpose down (0x08)
                            bool transposeDown = (flags & 0x08) == 0x08;
                            // bit 4 = Pause/Note (0x10)
                            bool noteOn = !((flags & 0x10) == 0x10);

                            // convert the rebirth note value to x0x note value...

                            if (step == pattern.Length)
                            {
                                // x0xbox doesn't store explicit pattern length,
                                // instead we have to use an end of pattern marker step
                                note = Step.X0X_EOP;
                            }
                            else if (noteOn)
                            {
                                note += Step.X0X_C2;
                                if (transposeUp)
                                {
                                    note += Step.OCTAVE;
                                }
                                if (transposeDown)
                                {
                                    note -= Step.OCTAVE;
                                }
                            }
                            else
                            {
                                // make that a x0x rest
                                note = Step.X0X_REST;
                            }

                            pattern[step] = new Step(note, slide, accent);
                        }

                        // one more pattern done
                        _patterns.Add(pattern);
                    }
                    _reader.ReadByte();//???
                }
                _reader.Close();

                // verify that we've read enough patterns
                if (_patterns.Count != PATTERNS_PER_303 * NUMBER_OF_303S)
                {
                    throw new Exception("Failed to read enough patterns");
                }
            }
        }

        public Pattern[] Patterns
        {
            get
            {
                return _patterns.ToArray();
            }
        }

        static private void VerifyTagMatches(string tag, string tagToMatch)
        {
            if (tag != tagToMatch)
            {
                throw new Exception("Expected: '" + tagToMatch + "'. Found '" + tag + "' instead.");
            }
        }

        static private void VerifyChunkLength(uint length, uint requiredLength)
        {
            if (length != requiredLength)
            {
                throw new Exception("Invalid chunk length. Expected: " + requiredLength + ". Found: " + length + ".");
            }
        }

        private string ReadChunkMarker()
        {
            Console.WriteLine("Reading chunk marker...");
            char[] chars = _reader.ReadChars(4);
            string chunk = new string(chars);
            Console.Write("Chunk marker = '" + chunk + "'.(");
            foreach (char c in chars)
            {
                Console.Write(Convert.ToInt16(c) + ",");
            }
            Console.WriteLine();
            return chunk;
        }

        private uint ReadChunkLength()
        {
            Console.WriteLine("Reading chunk length...");
            // RSB files are big endian, but BinaryRead works in little endian mode,
            // hence the conversion.
            uint l = Convert.ToUInt32(System.Net.IPAddress.HostToNetworkOrder(_reader.ReadInt32()));
            Console.WriteLine("Chunk length = " + l + " bytes.");
            return l;
        }

        private byte[] ReadChunk(uint length)
        {
            Console.WriteLine("Reading " + length + " bytes chunk...");
            byte[] chunk = _reader.ReadBytes(Convert.ToInt32(length));
            if (chunk.Length != Convert.ToInt32(length))
            {
                throw new EndOfStreamException("End of stream found trying to read " + length + " bytes.");
            }

            return chunk;
        }

    }
}
